/*
 * File:    Config.java
 * Created: 05-Nov-2005
 *
 * by Kenji Hollis <kenji@bitgatesoftware.com>
 * Released under the Creative Commons License version 2.5
 */

package com.bitgate.util.config;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.bitgate.util.file.FileUtil;

/**
 * Provides a way to load in a configuration file (or set of them) and apply an XML XPath expression against the loaded
 * configuration files.  These configuration files can then be loaded into memory, and stored in a centralized identifier.
 * Once loaded, the identifier is then used to reference the parsed XML files, and apply XPath expressions against them.
 * These XPath expressions are applied against XML files that are generally used for configuration settings, but they can
 * be used for other things, such as tweaks to classes, defaults, and so on.  Class access is used with
 * <code>bitgate.util.config.Config.getDefault()</code> since this class is a Singleton.
 *
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @since 0.1
 */
public class Config
{
    /**
     * This container is used to store information about a configuration file, as well as the XML document contained
     * therein.  It is used as an internal class, simply used to store the data stored in the XML files.
     */
    class ConfigContainer
    {
        private Document document;
        private final String filename, identifier;
        private long lastUpdate;
        private boolean isLoaded;

        public ConfigContainer(String filename, String identifier)
        {
            this.filename = filename;
            this.identifier = identifier;
            this.lastUpdate = 0L;
            this.isLoaded = false;

            reload();
        }

        public Document getDocument()
        {
            return this.document;
        }

        public String getFilename()
        {
            return this.filename;
        }

        public String getIdentifier()
        {
            return this.identifier;
        }

		public boolean documentLoaded()
		{
		    return this.isLoaded;
		}

        public void reload()
        {
            DocumentBuilder builder = null;
            
            try {
                builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            } catch(ParserConfigurationException e) {
                this.isLoaded = false;
            }

            File file = new File(this.filename);
            boolean updateNow = false;

            if (this.lastUpdate == 0 || this.lastUpdate != file.lastModified()) {
                updateNow = true;
            }
            
            if (updateNow) {
                try {
                	if (builder != null) {
		                this.document = builder.parse(FileUtil.getStream(this.filename));
		                this.isLoaded = true;
                	}
                } catch(SAXException e) {
                    this.isLoaded = false;
                } catch(IOException e) {
                    this.isLoaded = false;
                } catch(IllegalArgumentException e) {
                	this.isLoaded = false;
                }
                
                this.lastUpdate = file.lastModified();
            }
        }
    }

    private final HashMap<String, ConfigContainer> cContainer;
    private final static Config _default = new Config();
    
    /**
     * This is the constructor, however, it is used internally and started internally.  It will never be actually instantiated
     * by the end user.  This is for the Singleton instantiation.
     */
    private Config()
    {
        this.cContainer = new HashMap<String, ConfigContainer>();
    }

    /**
     * Returns the default instance of the <code>Config</code> class.
     *
     * @return <code>Config</code> default instance.
     */
    public static Config getDefault()
    {
        return _default;
    }

    /**
     * Registers a configuration filename to an identifier.  It replaces the same identifier with the same configuration
     * file (parsed at registration time) with a new node set, which is parsed from the filename specified.  Each filename
     * that is registered must be an XML file.  If it is not, the registration fails, and an error message is generated
     * in the log.
     *
     * @param filename The XML filename to load in.
     * @param identifier The identifier to register the specified XML file to.
     */
    public void register(String filename, String identifier)
    {
        this.cContainer.put(identifier, new ConfigContainer(filename, identifier));
    }

    /**
     * Determines whether or not a configuration entry has been loaded in memory.  This is used to make sure the configuration
     * file is available for use.  If you determine the file is not in memory, you can then take preventative measures to
     * make sure a config is not looked up (ie. using a default) or loaded at the time it needs to be used.
     *
     * @param identifier The identifier to look up.
     * @return <code>true</code> if the config is available, <code>false</code> otherwise.
     */
    public boolean isRegistered(String identifier)
    {
		if (this.cContainer.get(identifier) == null) {
		    return false;
		}
	
		if (this.cContainer.get(identifier).documentLoaded()) {
		    return true;
		}
	
		return false;
    }

    /**
     * Performs an XPath evaluation against a loaded configuration file.
     *
     * @param identifier The identifier of the XML node based on the <code>register</code>ed name.
     * @param node The XML XPath expression to apply to that node.
     * @return <code>String</code> containing the contents of the XPath expression.  Returns <code>null</code> on any errors.
     */
    public String parseConfig(String identifier, String node)
    {
        ConfigContainer cCont;
        
        if ((cCont = this.cContainer.get(identifier)) != null) {
            Document doc = cCont.getDocument();
            XPath xpath = XPathFactory.newInstance().newXPath();
            String xpathEval = null;

            try {
                xpathEval = (String) xpath.evaluate(node, doc, XPathConstants.STRING);
            } catch(XPathExpressionException e) {
                return null;
            }

            return xpathEval;
        }

        return null;
    }
    
    /**
     * Returns the node under which an XPath expression exists.
     * 
     * @param identifier The identifier of the XML node based onthe <code>register</code>ed name.
     * @param node The XML XPath expression to apply to that node.
     * @return <code>Node</code> containing the contents of the XPath expression.  Returns <code>null</code> on any errors.
     */
    public Node getNode(String identifier, String node)
    {
    	ConfigContainer cCont;
    	
    	if ((cCont = this.cContainer.get(identifier)) != null) {
    		Document doc = cCont.getDocument();
    		XPath xpath = XPathFactory.newInstance().newXPath();
    		Node xpathEval = null;
    		
    		try {
    			xpathEval = (Node) xpath.evaluate(node, doc, XPathConstants.NODE);
            } catch(XPathExpressionException e) {
                return null;
            }

            return xpathEval;
        }

        return null;
    }
}
